// 2010年版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

/*
Package zip provides support for reading and writing ZIP archives.

See: https:// www.pkware.com/appnote

This package does not support disk spanning.

A note about ZIP64:

To be backwards compatible the FileHeader has both 32 and 64 bit Size
fields. The 64 bit fields will always contain the correct value and
for normal archives both fields will be the same. For files requiring
the ZIP64 format the 32 bit fields will be 0xffffffff and the 64 bit
fields must be used instead.
*/
package zip

import (
	"io/fs"
	"path"
	"time"
)

// 压缩方法。
const (
	Store   uint16 = 0 // 无压缩
	Deflate uint16 = 8 // 放气压缩
)

const (
	fileHeaderSignature      = 0x04034b50
	directoryHeaderSignature = 0x02014b50
	directoryEndSignature    = 0x06054b50
	directory64LocSignature  = 0x07064b50
	directory64EndSignature  = 0x06064b50
	dataDescriptorSignature  = 0x08074b50 // 事实标准；OSX Finder所需
	fileHeaderLen            = 30         // +文件名+额外文件名
	directoryHeaderLen       = 46         // +文件名+附加+注释
	directoryEndLen          = 22         // +评论
	dataDescriptorLen        = 16         // 四个uint32：描述符签名，crc32，压缩大小，大小
	dataDescriptor64Len      = 24         // 具有8字节大小的描述符
	directory64LocLen        = 20         // None
	directory64EndLen        = 56         // +额外的

	// CreatorVersion中第一个字节的常量。
	creatorFAT    = 0
	creatorUnix   = 3
	creatorNTFS   = 11
	creatorVFAT   = 14
	creatorMacOSX = 19

	// 版本号。
	zipVersion20 = 20 // 2
	zipVersion45 = 45 // 4.5（读取和写入zip64档案）

	// 非zip64文件的限制。
	uint16max = (1 << 16) - 1
	uint32max = (1 << 32) - 1

	// 额外的标题ID。
	// None
	// ID 0..31保留供PKWARE正式使用。
	// 超过该范围的ID由第三方供应商定义。
	// 因为ZIP缺少高精度的时间戳（也没有官方规范）
	// 对于日期字段所使用的时区），许多其他字段相互竞争
	// 已经发明了。普遍使用有效地使它们成为“官方的”。
	// None
	// 见http：
	zip64ExtraID       = 0x0001 // Zip64扩展信息
	ntfsExtraID        = 0x000a // NTFS
	unixExtraID        = 0x000d // UNIX
	extTimeExtraID     = 0x5455 // 扩展时间戳
	infoZipUnixExtraID = 0x5855 // Info ZIP Unix扩展
)

// FileHeader描述zip文件中的文件。
// 有关详细信息，请参见zip规范。
type FileHeader struct {
	// Name是文件的名称。
	// None
	// 它必须是相对路径，而不是以驱动器号（如“C:”）开头，
	// 并且必须使用前斜杠而不是后斜杠。尾随斜杠
	// 指示此文件是一个目录，不应包含任何数据。
	// None
	// 读取zip文件时，名称字段将从中填充
	// zip文件直接复制，未验证其正确性。
	// 打电话的人有责任按照要求对其进行消毒
	// 适当，包括规范化斜杠方向，
	// 验证路径是否是相对的，并阻止路径
	// 遍历文件名（“../../../”）。
	Name string

	// 注释是任何短于64KiB的任意用户定义字符串。
	Comment string

	// 非UTF8表示名称和注释未在UTF-8中编码。
	// None
	// 根据规范，唯一允许的其他编码应为CP-437，
	// 但从历史上看，许多ZIP读者将姓名和评论解读为任何东西
	// 系统的本地字符编码恰好是。
	// None
	// 仅当用户打算对非便携设备进行编码时，才应设置此标志
	// 特定本地化区域的ZIP文件。否则，作者
	// 自动为有效的UTF-8字符串设置ZIP格式的UTF-8标志。
	NonUTF8 bool

	CreatorVersion uint16
	ReaderVersion  uint16
	Flags          uint16

	// 方法是压缩方法。如果为零，则使用存储。
	Method uint16

	// Modified是文件的修改时间。
	// None
	// 读取时，与传统MS-DOS相比，最好使用扩展时间戳
	// 日期字段，时间之间的偏移量用作时区。
	// 如果仅存在MS-DOS日期，则假定时区为UTC。
	// None
	// 在写入时，将使用扩展的时间戳（与时区无关）
	// 总是发射。传统MS-DOS日期字段根据
	// 修改时间的位置。
	Modified     time.Time
	ModifiedTime uint16 // 已弃用：旧版MS-DOS日期；改为使用修改的。
	ModifiedDate uint16 // 不推荐使用：遗留MS-DOS时间；改为使用修改的。

	CRC32              uint32
	CompressedSize     uint32 // 不推荐使用：改用CompressedSize64。
	UncompressedSize   uint32 // 不推荐使用：改用未压缩的DSIZE64。
	CompressedSize64   uint64
	UncompressedSize64 uint64
	Extra              []byte
	ExternalAttrs      uint32 // 意义取决于创作者的版本
}

// FileInfo为FileHeader返回fs.FileInfo。
func (h *FileHeader) FileInfo() fs.FileInfo {
	return headerFileInfo{h}
}

// headerFileInfo实现fs.FileInfo。
type headerFileInfo struct {
	fh *FileHeader
}

func (fi headerFileInfo) Name() string { return path.Base(fi.fh.Name) }
func (fi headerFileInfo) Size() int64 {
	if fi.fh.UncompressedSize64 > 0 {
		return int64(fi.fh.UncompressedSize64)
	}
	return int64(fi.fh.UncompressedSize)
}
func (fi headerFileInfo) IsDir() bool { return fi.Mode().IsDir() }
func (fi headerFileInfo) ModTime() time.Time {
	if fi.fh.Modified.IsZero() {
		return fi.fh.ModTime()
	}
	return fi.fh.Modified.UTC()
}
func (fi headerFileInfo) Mode() fs.FileMode { return fi.fh.Mode() }
func (fi headerFileInfo) Type() fs.FileMode { return fi.fh.Mode().Type() }
func (fi headerFileInfo) Sys() interface{}  { return fi.fh }

func (fi headerFileInfo) Info() (fs.FileInfo, error) { return fi, nil }

// FileInfoHeader从中创建部分填充的FileHeader
// fs.FileInfo。
// 因为fs.FileInfo的Name方法只返回
// 对于它描述的文件，可能需要修改名称字段
// 返回的标头的名称，以提供文件的完整路径名。
// 如果需要压缩，调用方应该设置FileHeader.Method
// 领域默认情况下，它是未设置的。
func FileInfoHeader(fi fs.FileInfo) (*FileHeader, error) {
	size := fi.Size()
	fh := &FileHeader{
		Name:               fi.Name(),
		UncompressedSize64: uint64(size),
	}
	fh.SetModTime(fi.ModTime())
	fh.SetMode(fi.Mode())
	if fh.UncompressedSize64 > uint32max {
		fh.UncompressedSize = uint32max
	} else {
		fh.UncompressedSize = uint32(fh.UncompressedSize64)
	}
	return fh, nil
}

type directoryEnd struct {
	diskNbr            uint32 // 未使用
	dirDiskNbr         uint32 // 未使用
	dirRecordsThisDisk uint64 // 未使用
	directoryRecords   uint64
	directorySize      uint64
	directoryOffset    uint64 // 相对于文件
	commentLen         uint16
	comment            string
}

// 时区根据提供的偏移量返回*time.Location。
// 如果偏移量不合理，则使用零偏移量。
func timeZone(offset time.Duration) *time.Location {
	const (
		minOffset   = -12 * time.Hour  // 例如，贝克岛-12:00
		maxOffset   = +14 * time.Hour  // 例如，+14:00时的线路岛
		offsetAlias = 15 * time.Minute // 例如，尼泊尔时间+5:45
	)
	offset = offset.Round(offsetAlias)
	if offset < minOffset || maxOffset < offset {
		offset = 0
	}
	return time.FixedZone("", int(offset/time.Second))
}

// MSDOSTIMOTIME将MS-DOS日期和时间转换为time.time。
// 分辨率为2s。
// 请参阅：https:
func msDosTimeToTime(dosDate, dosTime uint16) time.Time {
	return time.Date(
		// 日期位0-4：月份的第几天；5-8：月；9-15：1980年以来的年份
		int(dosDate>>9+1980),
		time.Month(dosDate>>5&0xf),
		int(dosDate&0x1f),

		// 时间位0-4：秒/2；5-10分钟；11-15:00
		int(dosTime>>11),
		int(dosTime>>5&0x3f),
		int(dosTime&0x1f*2),
		0, // 纳秒

		time.UTC,
	)
}

// timeToMsDosTime将time.time转换为MS-DOS日期和时间。
// 分辨率为2s。
// 请参阅：https:
func timeToMsDosTime(t time.Time) (fDate uint16, fTime uint16) {
	fDate = uint16(t.Day() + int(t.Month())<<5 + (t.Year()-1980)<<9)
	fTime = uint16(t.Second()/2 + t.Minute()<<5 + t.Hour()<<11)
	return
}

// ModTime使用旧版返回修改时间（UTC）
// ModifiedDate和ModifiedTime字段。
// None
// 不推荐使用：改用“已修改”。
func (h *FileHeader) ModTime() time.Time {
	return msDosTimeToTime(h.ModifiedDate, h.ModifiedTime)
}

// SetModTime设置Modified、ModifiedTime和ModifiedDate字段
// 以UTC表示的给定时间。
// None
// 不推荐使用：改用“已修改”。
func (h *FileHeader) SetModTime(t time.Time) {
	t = t.UTC() // 转换为UTC以实现兼容性
	h.Modified = t
	h.ModifiedDate, h.ModifiedTime = timeToMsDosTime(t)
}

const (
	// Unix常量。说明书上没有提到它们，
	// 但这些似乎是工具所认同的价值观。
	s_IFMT   = 0xf000
	s_IFSOCK = 0xc000
	s_IFLNK  = 0xa000
	s_IFREG  = 0x8000
	s_IFBLK  = 0x6000
	s_IFDIR  = 0x4000
	s_IFCHR  = 0x2000
	s_IFIFO  = 0x1000
	s_ISUID  = 0x800
	s_ISGID  = 0x400
	s_ISVTX  = 0x200

	msdosDir      = 0x10
	msdosReadOnly = 0x01
)

// 模式返回FileHeader的权限和模式位。
func (h *FileHeader) Mode() (mode fs.FileMode) {
	switch h.CreatorVersion >> 8 {
	case creatorUnix, creatorMacOSX:
		mode = unixModeToFileMode(h.ExternalAttrs >> 16)
	case creatorNTFS, creatorVFAT, creatorFAT:
		mode = msdosModeToFileMode(h.ExternalAttrs)
	}
	if len(h.Name) > 0 && h.Name[len(h.Name)-1] == '/' {
		mode |= fs.ModeDir
	}
	return mode
}

// SetMode更改FileHeader的权限和模式位。
func (h *FileHeader) SetMode(mode fs.FileMode) {
	h.CreatorVersion = h.CreatorVersion&0xff | creatorUnix<<8
	h.ExternalAttrs = fileModeToUnixMode(mode) << 16

	// 也设置MSDOS属性，就像原始zip一样。
	if mode&fs.ModeDir != 0 {
		h.ExternalAttrs |= msdosDir
	}
	if mode&0200 == 0 {
		h.ExternalAttrs |= msdosReadOnly
	}
}

// isZip64报告文件大小是否超过32位限制
func (h *FileHeader) isZip64() bool {
	return h.CompressedSize64 >= uint32max || h.UncompressedSize64 >= uint32max
}

func msdosModeToFileMode(m uint32) (mode fs.FileMode) {
	if m&msdosDir != 0 {
		mode = fs.ModeDir | 0777
	} else {
		mode = 0666
	}
	if m&msdosReadOnly != 0 {
		mode &^= 0222
	}
	return mode
}

func fileModeToUnixMode(mode fs.FileMode) uint32 {
	var m uint32
	switch mode & fs.ModeType {
	default:
		m = s_IFREG
	case fs.ModeDir:
		m = s_IFDIR
	case fs.ModeSymlink:
		m = s_IFLNK
	case fs.ModeNamedPipe:
		m = s_IFIFO
	case fs.ModeSocket:
		m = s_IFSOCK
	case fs.ModeDevice:
		if mode&fs.ModeCharDevice != 0 {
			m = s_IFCHR
		} else {
			m = s_IFBLK
		}
	}
	if mode&fs.ModeSetuid != 0 {
		m |= s_ISUID
	}
	if mode&fs.ModeSetgid != 0 {
		m |= s_ISGID
	}
	if mode&fs.ModeSticky != 0 {
		m |= s_ISVTX
	}
	return m | uint32(mode&0777)
}

func unixModeToFileMode(m uint32) fs.FileMode {
	mode := fs.FileMode(m & 0777)
	switch m & s_IFMT {
	case s_IFBLK:
		mode |= fs.ModeDevice
	case s_IFCHR:
		mode |= fs.ModeDevice | fs.ModeCharDevice
	case s_IFDIR:
		mode |= fs.ModeDir
	case s_IFIFO:
		mode |= fs.ModeNamedPipe
	case s_IFLNK:
		mode |= fs.ModeSymlink
	case s_IFREG:
		// 无事可做
	case s_IFSOCK:
		mode |= fs.ModeSocket
	}
	if m&s_ISGID != 0 {
		mode |= fs.ModeSetgid
	}
	if m&s_ISUID != 0 {
		mode |= fs.ModeSetuid
	}
	if m&s_ISVTX != 0 {
		mode |= fs.ModeSticky
	}
	return mode
}
